﻿using System;
using Engine.Math;
using Microsoft.Xna.Framework;

namespace Engine.Terrain
{
    [Serializable]
    public class HeightMapTerrain
    {
        public HeightMapTerrain(HeightMapParameters pars, int vertexCountX, int vertexCountZ)
        {
            Parameters = pars;
            VertexCountX = vertexCountX;
            VertexCountZ = vertexCountZ;
        }

        #region Properties

        public HeightMapParameters Parameters { get; private set; }
        public int VertexCountX { get; private set; }
        public int VertexCountZ { get; private set; }
        
        public float[,] Heights { get; internal set; }
        public VertexPositionTexture2Normal[] Vertices { get; internal set; }
        public int[] Indices { get; internal set; }  

        public Vector3 HeightMapPosition
        {
            get
            {
                float x = -(VertexCountX - 1)/2f*Parameters.BlockSize;
                float z = -(VertexCountZ - 1)/2f*Parameters.BlockSize;
                return new Vector3(x, 0, z);
            }
        }
        public float HeightmapWidth
        {
            get { return (VertexCountX - 1)*Parameters.BlockSize; }
        }
        public float HeightmapHeight
        {
            get { return (VertexCountZ - 1)*Parameters.BlockSize; }
        }

        #endregion

        #region Custom Types
        
        #endregion

        #region Public Methods

        public int GetVertexIndex(int x,int y)
        {
            return VertexCountZ*y + x;
        }
        public void GetNormal(Vector3 position, out Vector3 normal)
        {
            // the first thing we need to do is figure out where on the heightmap
            // "position" is. This'll make the math much simpler later.
            Vector3 positionOnHeightmap = position - HeightMapPosition;

            // we'll use integer division to figure out where in the "heights" array
            // positionOnHeightmap is. Remember that integer division always rounds
            // down, so that the result of these divisions is the indices of the "upper
            // left" of the 4 corners of that cell.
            int left, top;
            left = (int) positionOnHeightmap.X/(int) Parameters.BlockSize;
            top = (int) positionOnHeightmap.Z/(int) Parameters.BlockSize;

            // next, we'll use modulus to find out how far away we are from the upper
            // left corner of the cell. Mod will give us a value from 0 to terrainScale,
            // which we then divide by terrainScale to normalize 0 to 1.
            float xNormalized = (positionOnHeightmap.X%Parameters.BlockSize)/Parameters.BlockSize;
            float zNormalized = (positionOnHeightmap.Z%Parameters.BlockSize)/Parameters.BlockSize;
              

            // We'll repeat the same process to calculate the normal.
            Vector3 topNormal = Vector3.Lerp(
                Vertices[GetVertexIndex(left, top)].Normal,
                Vertices[GetVertexIndex(left + 1, top)].Normal,
                xNormalized);

            Vector3 bottomNormal = Vector3.Lerp(
                Vertices[GetVertexIndex(left, top + 1)].Normal,
                Vertices[GetVertexIndex(left + 1, top + 1)].Normal,
                xNormalized);

            normal = Vector3.Lerp(topNormal, bottomNormal, zNormalized);
            normal.Normalize();
        }
        public CollisionInfo Intersects(Ray ray)
        {
            Vector3 rayStep = ray.Direction * Parameters.BlockSize * 0.5f;
            Vector3 rayStartPosition = ray.Position;

            // Linear search. Loop until find a point inside the terrain
            Vector3 lastRayPosition = ray.Position;
            ray.Position += rayStep;
            float height = GetHeight(ray.Position);
            while (ray.Position.Y > height && height != float.MaxValue)
            {
                lastRayPosition = ray.Position;
                ray.Position += rayStep;
                height = GetHeight(ray.Position);

            }
            // If the ray collide with the terrain
            if (height != float.MaxValue)
            {
                Vector3 startPosition = lastRayPosition;
                Vector3 endPosition = ray.Position;
                // Binary search. Find the exact collision point
                for (int i = 0; i < 32; i++)
                {
                    // Bynary search pass
                    Vector3 middlePoint = (startPosition + endPosition) * 0.5f;
                    if (middlePoint.Y < height)
                        endPosition = middlePoint;
                    else
                        startPosition = middlePoint;
                }
                Vector3 collisionPoint = (startPosition + endPosition) * 0.5f;
                var collisionInfo = new CollisionInfo();
                collisionInfo.Position = collisionPoint;
                collisionInfo.Distance = Vector3.Distance(collisionPoint, rayStartPosition);
                return collisionInfo;
            }
            return null;
        }
        public bool IsOnHeightmap(Vector3 position)
        {            
            Vector3 positionOnHeightmap = position - HeightMapPosition;          

            return (positionOnHeightmap.X > 0 &&
                positionOnHeightmap.X < HeightmapWidth &&
                positionOnHeightmap.Z > 0 &&
                positionOnHeightmap.Z < HeightmapHeight);
        }    
        public float GetHeight(Vector3 position)
        {
            Vector3 positionOnHeightmap = position - HeightMapPosition;
            int left = (int) positionOnHeightmap.X/(int) Parameters.BlockSize;
            int top = (int) positionOnHeightmap.Z/(int) Parameters.BlockSize;
            if (left >= Heights.GetLength(0) - 1 || left < 0)
                return float.MaxValue;
            if (top >= Heights.GetLength(1) - 1 || top < 0)
                return float.MaxValue;

            float xNormalized = (positionOnHeightmap.X%Parameters.BlockSize)/Parameters.BlockSize;
            float zNormalized = (positionOnHeightmap.Z%Parameters.BlockSize)/Parameters.BlockSize;

            float topHeight = MathHelper.Lerp(
                Heights[left, top],
                Heights[left + 1, top],
                xNormalized);

            float bottomHeight = MathHelper.Lerp(
                Heights[left, top + 1],
                Heights[left + 1, top + 1],
                xNormalized);

            return MathHelper.Lerp(topHeight, bottomHeight, zNormalized);
        }

        #endregion

    }
}
